package Objects;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;

public class Router {
	public static Config myConfig;
	public static Neighbors myNeighbors;
	public static RoutingTable myRoutingTable = new RoutingTable();
	public static ConcurrentHashMap<String, Long> aliveNodes = new ConcurrentHashMap<String, Long>();

	public static void init() {
		populateRoutingTable();
		Receiver r = new Receiver();
		Thread t = new Thread(r);
		t.start();
		DeadRouterChecker d = new DeadRouterChecker();
		Thread t1 = new Thread(d);
		t1.start();
		HelloSender h = new HelloSender();
		Thread t2 = new Thread(h);
		t2.start();
	}

	public static void printRoutingTable() {
		Logger.log(Logger.ALL, myRoutingTable);
	}

	public static ArrayList<String> getReachableDestination() {
		Iterator<String> it = myRoutingTable.getDestination().keySet().iterator();
		ArrayList<String> reachableDestination = new ArrayList<String>();
		while (it.hasNext()) {
			reachableDestination.add(it.next());
		}
		return reachableDestination;
	}

	private static void populateRoutingTable() {
		myRoutingTable.setMyID(myConfig.getRouterID());
		myRoutingTable.setMyIG(myConfig.getIG());
		for (int i = 0; i < myNeighbors.getMyNeighbors().size(); i++) {
			Path path = new Path();
			PathInfo pathInfo = new PathInfo();
			String sourceID = myConfig.getRouterID();
			String destinationID = myNeighbors.getMyNeighbors().get(i).getRouterID();
			char destinationIG = myNeighbors.getMyNeighbors().get(i).getIG();
			ArrayList<String> pathArray = new ArrayList<String>();
			pathArray.add(sourceID);
			pathArray.add(destinationID);
			int hopCount = 1;
			path.setDestination(destinationID);
			path.setSource(sourceID);
			path.setHopCount(hopCount);
			path.setPath(pathArray);
			pathInfo.setHopCount(hopCount);
			pathInfo.setIG(destinationIG);
			pathInfo.setBestPath(path);
			// System.out.println(pathInfo);
			myRoutingTable.getDestination().put(destinationID, pathInfo);
			if (myRoutingTable.getMyIG() == 'g') {
				if (myNeighbors.getMyNeighbors().get(i).getIG() == 'i') {
					myRoutingTable.addIntraRouter(myNeighbors.getMyNeighbors().get(i).getRouterID());
				}
			}
		}
	}

	private static void sendHelloMessages() {
		ArrayList<String> myNeighborsIPs = myNeighbors.getAllNeighborIPs();

		// loop through my neighbors to send the Hello Message
		for (int i = 0; i < myNeighborsIPs.size(); i++) {
			if (myNeighborsIPs.get(i) == null) {
				// do not have neighbor's ip, do not send hello message
			} else {
				Sender helloSender = new Sender(myNeighborsIPs.get(i), 9876, myConfig.getRouterID() + ", "
						+ myConfig.getIG() + ", Hello", "hello");
				Thread t = new Thread(helloSender);
				t.start();
			}
		}
	}

	public static void sendDataMessage(String sourceID, String desID, String message) {
		if (sourceID.equals(desID)) {
			Logger.log(Logger.ALL, "Message from myself: " + message);
			return;
		}

		
		String toSend = sourceID + ", " + desID + ", " + message + ", Data";
		String nextHop = myRoutingTable.getNextHop(desID);
		Logger.log(Logger.ALL, "Next hop is: "+nextHop);
		if (!nextHop.equals("")) {
			String nextHopIP = myNeighbors.getNeighborByID(nextHop).getIP();
			Sender dataSender = new Sender(nextHopIP, 9876, toSend, "data");
			Thread t = new Thread(dataSender);
			t.start();
		} else {
			if (sourceID.equals(myRoutingTable.getMyID())) {
				Logger.log(Logger.ALL, "Cannot reach " + desID + ", Gateway router dead?");
			} else {
				toSend = myConfig.getRouterID() + ", " + sourceID + ", " + "Cannot reach " + desID + ", Data";
				nextHop = myRoutingTable.getNextHop(sourceID);
				String nextHopIP = myNeighbors.getNeighborByID(nextHop).getIP();
				Sender dataSender = new Sender(nextHopIP, 9876, toSend, "data");
				Thread t = new Thread(dataSender);
				t.start();
			}
		}
	}

	private static void sendWithdrawMessage(String message) {
		String deadRouter = message.split(",")[0].trim();
		ArrayList<String> myNeighborsIPs = myNeighbors.getAllNeighborIPs();

		Logger.log(Logger.DEBUG, myNeighbors.printList());
		for (int i = 0; i < myNeighborsIPs.size(); i++) {
			if (myNeighborsIPs.get(i) != null) {
				if (!Parser.getRouterID(myNeighborsIPs.get(i)).equals(deadRouter)) {
					Sender withdrawSender = new Sender(myNeighborsIPs.get(i), 9876, message, "withdraw");
					Thread t = new Thread(withdrawSender);
					t.start();
				}
			}
		}
	}

	private static class Receiver extends Thread {
		private Receiver() {
		}

		@Override
		public void run() {
			try {
				DatagramSocket serverSocket = new DatagramSocket(9876);
				byte[] receiveData = new byte[1024];
				byte[] sendData = new byte[10];
				while (true) {
					receiveData = new byte[1024];
					DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
					// System.out.println("waiting for datagram packet");

					serverSocket.receive(receivePacket);
					String data = new String(receivePacket.getData());

					InetAddress ipAddress = receivePacket.getAddress();
					int port = receivePacket.getPort();

					Logger.log(Logger.DEBUG, "====================================================");
					Logger.log(Logger.DEBUG, "From: " + ipAddress + ":" + port);
					Logger.log(Logger.DEBUG, "Message: " + data);
					Logger.log(Logger.DEBUG, "====================================================");

					String[] splittedData = data.split(",");

					String messageType = splittedData[splittedData.length - 1].trim();
					String senderIP = ipAddress.getHostAddress();
					char senderIG = splittedData[1].trim().charAt(0);
					String source = splittedData[0].trim();
					// Hello message, get the sender's IP, add to my neighbor's
					// IP so I can send Hello message to the sender
					if (messageType.equals("Hello")) {
						long currentTime = System.currentTimeMillis();
						aliveNodes.put(source, currentTime);
						Logger.log(Logger.DEBUG, aliveNodes);
						myNeighbors.getNeighborByID(source).setIP(senderIP);
						myNeighbors.getNeighborByID(source).setIG(senderIG);
						// System.out.println(myNeighbors);
					}
					// Update message
					else if (messageType.equals("Update")) {
						// System.out.println("Update message: " + data);
						myNeighbors.addNeighbor(source, senderIP);
						// Logger.log(Logger.DEBUG,
						// "Before parse: "+myRoutingTable.printMyIntraRouters());
						MessageParser.parseUpdateMessage(data, myRoutingTable);
						// Logger.log(Logger.DEBUG,
						// "After parse: "+myRoutingTable.printMyIntraRouters());
						myNeighbors.getNeighborByID(source).setIG(senderIG);
						myNeighbors.getNeighborByID(source).setIP(senderIP);
						// Logger.log(Logger.DEBUG, myRoutingTable);

					}
					// Intra router list update message
					else if (messageType.equals("Intra")) {
						MessageParser.parseIntraMessage(data, myRoutingTable);
						// Logger.log(Logger.DEBUG, myRoutingTable);
					}
					// Data message
					else if (messageType.equals("Data")) {
						String sourceNode = splittedData[0].trim();
						String desNode = splittedData[1].trim();
						String message = splittedData[2].trim();
						// if im the destination of this message, print it out
						if (myRoutingTable.getMyID().equals(desNode)) {
							Logger.log(Logger.ALL, "Message from: " + sourceNode + " - " + message);
						}
						// else pass to next hop
						else {
							Logger.log(Logger.ALL,
									"Got message from " + Parser.getRouterID(ipAddress.getHostAddress())
											+ " to " + desNode + ": " + message);
							sendDataMessage(sourceNode, desNode, message);
						}
					} else if (messageType.equals("Withdraw")) {
						if (!myRoutingTable.routerPurged(source, senderIG)) {
							sendWithdrawMessage(data);
							MessageParser.deadRouterPurge(source, myRoutingTable);
						}
					}

					sendData = "Ack".getBytes();
					DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, ipAddress, port);
					serverSocket.send(sendPacket);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

	}

	/**
	 * Sends one string of data This is used to send hello messages and data
	 * messages
	 * 
	 * @author jxa074000
	 * 
	 */
	private static class Sender extends Thread {
		String ip = "";
		int port = -1;
		String data = "";
		String type = "";

		private Sender(String ip, int port, String data, String type) {
			this.ip = ip;
			this.port = port;
			this.data = data;
			this.type = type;
		}

		public void run() {
			try {
				byte[] receiveData = new byte[10];
				DatagramSocket clientSocket = new DatagramSocket();
				InetAddress ipAddress = InetAddress.getByName(ip);
				Logger.log(
						Logger.DEBUG,
						"Trying to send packet: " + data + " to: "
								+ Parser.getRouterID(ipAddress.getHostAddress()) + " ("
								+ ipAddress.getHostAddress() + ")");
				DatagramPacket sendPacket = new DatagramPacket(data.getBytes(), data.getBytes().length,
						ipAddress, port);
				clientSocket.send(sendPacket);

				DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
				clientSocket.setSoTimeout(10000);

				clientSocket.receive(receivePacket);
				String ack = new String(receivePacket.getData());

				if (ack.trim().equals("Ack")) {
					Logger.log(Logger.DEBUG, "Ack recieved from " + Parser.getRouterID(ip) + " " + type
							+ " message");
				} else {
					Logger.log(Logger.DEBUG, "Supposed to be Ack from" + Parser.getRouterID(ip)
							+ ", but Recieved: " + ack);
				}
				clientSocket.close();
			} catch (SocketTimeoutException e) {
				Logger.log(Logger.DEBUG, "Ack from " + Parser.getRouterID(ip)
						+ " timeout occurred: Ack packet assume lost");
				// e.printStackTrace();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	private static class DeadRouterChecker extends Thread {
		public void run() {
			while (true) {
				try {
					Iterator<String> it = aliveNodes.keySet().iterator();
					while (it.hasNext()) {
						long currentTime = System.currentTimeMillis();
						String routerID = it.next();
						long lastAlive = aliveNodes.get(routerID);
						// Logger.log(Logger.DEBUG, aliveNodes);
						// the last hello message from current node was 20
						// seconds ago
						Logger.log(Logger.DEBUG, "Last alive: " + lastAlive + " Current time: " + currentTime);
						if (lastAlive + 20000 < currentTime) {
							Logger.log(Logger.DEBUG, "Time expired, assume router " + routerID + " is dead.");
							// assume that router is dead
							aliveNodes.remove(routerID);
							char deadNodeIG = myNeighbors.getNeighborByID(routerID).getIG();
							myNeighbors.removeNeighbor(routerID);
							sendWithdrawMessage(routerID + ", " + deadNodeIG + ", Withdraw");
							myRoutingTable = MessageParser.deadRouterPurge(routerID, myRoutingTable);
						}
					}
					Thread.sleep(10000);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}

	private static class HelloSender extends Thread {
		public void run() {
			while (true) {
				Router.sendHelloMessages();
				try {
					Thread.sleep(10000);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}
}
